/**
 * @fileoverview Tests for options.
 * @author Sindre Sorhus
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const assert = require("chai").assert,
	chalk = require("chalk"),
	proxyquire = require("proxyquire"),
	sinon = require("sinon");

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/*
 * Chalk protects its methods so we need to inherit from it
 * for Sinon to work.
 */
const chalkStub = Object.create(chalk, {
	reset: {
		value(str) {
			return chalk.reset(str);
		},
		writable: true,
	},
	yellow: {
		value(str) {
			return chalk.yellow(str);
		},
		writable: true,
	},
	red: {
		value(str) {
			return chalk.red(str);
		},
		writable: true,
	},
});

chalkStub.yellow.bold = chalk.yellow.bold;
chalkStub.red.bold = chalk.red.bold;

const formatter = proxyquire("../../../../lib/cli-engine/formatters/stylish", {
	chalk: chalkStub,
});

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

describe("formatter:stylish", () => {
	const originalColorLevel = chalk.level;

	beforeEach(() => {
		chalk.level = 0;
		sinon.spy(chalkStub, "reset");
		sinon.spy(chalkStub.yellow, "bold");
		sinon.spy(chalkStub.red, "bold");
	});

	afterEach(() => {
		sinon.verifyAndRestore();
		chalk.level = originalColorLevel;
	});

	describe("when passed no messages", () => {
		const code = [
			{
				filePath: "foo.js",
				messages: [],
				errorCount: 0,
				warningCount: 0,
			},
		];

		it("should not return message", () => {
			const result = formatter(code);

			assert.strictEqual(result, "");
			assert.strictEqual(chalkStub.reset.callCount, 0);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 0);
		});
	});

	describe("when passed a single error message", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 1,
				warningCount: 0,
				fixableErrorCount: 0,
				fixableWarningCount: 0,
				messages: [
					{
						message: "Unexpected foo.",
						severity: 2,
						line: 5,
						column: 10,
						ruleId: "foo",
					},
				],
			},
		];

		it("should return a string in the correct format", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  error  Unexpected foo  foo\n\n\u2716 1 problem (1 error, 0 warnings)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});

		describe("when the error is fixable", () => {
			beforeEach(() => {
				code[0].fixableErrorCount = 1;
			});

			it("should return a string in the correct format", () => {
				const result = formatter(code);

				assert.strictEqual(
					result,
					"\nfoo.js\n  5:10  error  Unexpected foo  foo\n\n\u2716 1 problem (1 error, 0 warnings)\n  1 error and 0 warnings potentially fixable with the `--fix` option.\n",
				);
				assert.strictEqual(chalkStub.reset.callCount, 1);
				assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
				assert.strictEqual(chalkStub.red.bold.callCount, 2);
			});
		});
	});

	describe("when passed a single warning message", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 0,
				warningCount: 1,
				fixableErrorCount: 0,
				fixableWarningCount: 0,
				messages: [
					{
						message: "Unexpected foo.",
						severity: 1,
						line: 5,
						column: 10,
						ruleId: "foo",
					},
				],
			},
		];

		it("should return a string in the correct format", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  warning  Unexpected foo  foo\n\n\u2716 1 problem (0 errors, 1 warning)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 1);
			assert.strictEqual(chalkStub.red.bold.callCount, 0);
		});

		describe("when the error is fixable", () => {
			beforeEach(() => {
				code[0].fixableWarningCount = 1;
			});

			it("should return a string in the correct format", () => {
				const result = formatter(code);

				assert.strictEqual(
					result,
					"\nfoo.js\n  5:10  warning  Unexpected foo  foo\n\n\u2716 1 problem (0 errors, 1 warning)\n  0 errors and 1 warning potentially fixable with the `--fix` option.\n",
				);
				assert.strictEqual(chalkStub.reset.callCount, 1);
				assert.strictEqual(chalkStub.yellow.bold.callCount, 2);
				assert.strictEqual(chalkStub.red.bold.callCount, 0);
			});
		});
	});

	describe("when passed a message that ends with ' .'", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 0,
				warningCount: 1,
				fixableErrorCount: 0,
				fixableWarningCount: 0,
				messages: [
					{
						message: "Unexpected .",
						severity: 1,
						line: 5,
						column: 10,
						ruleId: "foo",
					},
				],
			},
		];

		it("should return a string in the correct format (retaining the ' .')", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  warning  Unexpected .  foo\n\n\u2716 1 problem (0 errors, 1 warning)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 1);
			assert.strictEqual(chalkStub.red.bold.callCount, 0);
		});
	});

	describe("when passed a fatal error message", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 1,
				warningCount: 0,
				messages: [
					{
						fatal: true,
						message: "Unexpected foo.",
						line: 5,
						column: 10,
						ruleId: "foo",
					},
				],
			},
		];

		it("should return a string in the correct format", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  error  Unexpected foo  foo\n\n\u2716 1 problem (1 error, 0 warnings)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});
	});

	describe("when passed multiple messages", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 1,
				warningCount: 1,
				messages: [
					{
						message: "Unexpected foo.",
						severity: 2,
						line: 5,
						column: 10,
						ruleId: "foo",
					},
					{
						message: "Unexpected bar.",
						severity: 1,
						line: 6,
						column: 11,
						ruleId: "bar",
					},
				],
			},
		];

		it("should return a string with multiple entries", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  error    Unexpected foo  foo\n  6:11  warning  Unexpected bar  bar\n\n\u2716 2 problems (1 error, 1 warning)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});
	});

	describe("when passed multiple files with 1 message each", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 1,
				warningCount: 0,
				messages: [
					{
						message: "Unexpected foo.",
						severity: 2,
						line: 5,
						column: 10,
						ruleId: "foo",
					},
				],
			},
			{
				errorCount: 0,
				warningCount: 1,
				filePath: "bar.js",
				messages: [
					{
						message: "Unexpected bar.",
						severity: 1,
						line: 6,
						column: 11,
						ruleId: "bar",
					},
				],
			},
		];

		it("should return a string with multiple entries", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  error  Unexpected foo  foo\n\nbar.js\n  6:11  warning  Unexpected bar  bar\n\n\u2716 2 problems (1 error, 1 warning)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});

		it("should add errorCount", () => {
			code.forEach(c => {
				c.errorCount = 1;
				c.warningCount = 0;
			});

			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  error  Unexpected foo  foo\n\nbar.js\n  6:11  warning  Unexpected bar  bar\n\n\u2716 2 problems (2 errors, 0 warnings)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});

		it("should add warningCount", () => {
			code.forEach(c => {
				c.errorCount = 0;
				c.warningCount = 1;
			});

			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  5:10  error  Unexpected foo  foo\n\nbar.js\n  6:11  warning  Unexpected bar  bar\n\n\u2716 2 problems (0 errors, 2 warnings)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});
	});

	describe("when passed one file not found message", () => {
		const code = [
			{
				filePath: "foo.js",
				errorCount: 1,
				warningCount: 0,
				messages: [
					{
						fatal: true,
						message: "Couldn't find foo.js.",
					},
				],
			},
		];

		it("should return a string without line and column", () => {
			const result = formatter(code);

			assert.strictEqual(
				result,
				"\nfoo.js\n  0:0  error  Couldn't find foo.js\n\n\u2716 1 problem (1 error, 0 warnings)\n",
			);
			assert.strictEqual(chalkStub.reset.callCount, 1);
			assert.strictEqual(chalkStub.yellow.bold.callCount, 0);
			assert.strictEqual(chalkStub.red.bold.callCount, 1);
		});
	});

	describe("fixable problems", () => {
		it("should not output fixable problems message when no errors or warnings are fixable", () => {
			const code = [
				{
					filePath: "foo.js",
					errorCount: 1,
					warningCount: 0,
					fixableErrorCount: 0,
					fixableWarningCount: 0,
					messages: [
						{
							message: "Unexpected foo.",
							severity: 2,
							line: 5,
							column: 10,
							ruleId: "foo",
						},
					],
				},
			];

			const result = formatter(code);

			assert.notInclude(result, "potentially fixable");
		});

		it("should output the fixable problems message when errors are fixable", () => {
			const code = [
				{
					filePath: "foo.js",
					errorCount: 1,
					warningCount: 0,
					fixableErrorCount: 1,
					fixableWarningCount: 0,
					messages: [
						{
							message: "Unexpected foo.",
							severity: 2,
							line: 5,
							column: 10,
							ruleId: "foo",
						},
					],
				},
			];

			const result = formatter(code);

			assert.include(
				result,
				"  1 error and 0 warnings potentially fixable with the `--fix` option.\n",
			);
		});

		it("should output fixable problems message when warnings are fixable", () => {
			const code = [
				{
					filePath: "foo.js",
					errorCount: 0,
					warningCount: 3,
					fixableErrorCount: 0,
					fixableWarningCount: 2,
					messages: [
						{
							message: "Unexpected foo.",
						},
					],
				},
			];

			const result = formatter(code);

			assert.include(
				result,
				"  0 errors and 2 warnings potentially fixable with the `--fix` option.\n",
			);
		});

		it("should output the total number of fixable errors and warnings", () => {
			const code = [
				{
					filePath: "foo.js",
					errorCount: 5,
					warningCount: 3,
					fixableErrorCount: 5,
					fixableWarningCount: 2,
					messages: [
						{
							message: "Unexpected foo.",
						},
					],
				},
				{
					filePath: "bar.js",
					errorCount: 4,
					warningCount: 2,
					fixableErrorCount: 4,
					fixableWarningCount: 1,
					messages: [
						{
							message: "Unexpected bar.",
						},
					],
				},
			];

			const result = formatter(code);

			assert.include(
				result,
				"  9 errors and 3 warnings potentially fixable with the `--fix` option.\n",
			);
		});
	});
});
